几种相对复杂的 DOM 操作技术

1. 动态脚本的添加

动态脚本是指,在页面加载时不存在,当页面加载完毕后,通过 DOM 操作动态添加的脚本。

脚本的添加形式有两种:

  1. 通过 <script> 标签的 src 特性引入外部资源
    动态加载的外部 JavaScript 文件能够立即执行,比如下面的 <script> 元素:

    <script type="text/javascript" src="test.js"></script>

    创建这个节点的 DOM 代码如下:

    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = "test.js";
    document.body.appendChild(script);

    也可以把这个过程封装到一个函数里:

    function addScript(url) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = url;
    document.body.appendChild(script);
    }
    // 然后,可以通过调用这个函数来加载外部脚本文件:
    addScript("test.js");
  2. 直接在 <script> 标签中书写脚本代码,也称行内式
    比如下面的节点:

    <script type="text/javascript">
    function sayHi() {
    alert("hi");
    }
    </script>

    创建这个节点的 DOM 代码如下:

    var script = document.createElement("script");
    script.type = "text/javascript";
    script.appendChild(document.createTextNode("function sayHi() {alert('hi');}"));
    document.dody.appendChild(script);

    在大多数浏览器中,上面的代码都可以正常执行,但是在 IE 中,则会导致错误。因为 IE 将 <script> 视为一个特殊的元素节点,不允许 DOM 访问其子节点。不过,可以使用 <script> 元素的 text 属性来指定 JavaScript 代码,像下面这样:

    var script = document.createElement("script");
    script.type = "text/javascript";
    script.text = "function sayHi() {alert('hi');}";
    document.body.appendChild(script);

    下面的方法可以兼容所有的浏览器:

    function loadScriptString(code) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    try {
    script.appendChild(document.createTextNode(code));
    } catch (error) {
    script.text = code;
    }
    document.body.appendChild(script);
    }
    // 然后,可以通过调用这个函数来加载指定的代码
    loadScriptString("function sayHi() {alert('hi');}");

    这里,首先尝试标准的 DOM 文本节点方法,除了 IE,其它浏览器都会采用这种方式。如果代码抛出了错误,那么说明是 IE,于是就必须使用 text 属性了。

2. 动态样式的添加

与动态脚本类似,所谓动态样式就是在页面加载的时候不存在,在页面加载完成后动态添加到页面中的样式。

样式的添加同样有两种方法,一种是添加外部样式文件,一种是行内式添加。不过跟脚本不一样的是,两种方法用的元素标签是不一样的。

  1. 通过 <link> 标签添加外部样式
    我们以下面的这个 <link> 元素为例:

    <link rel="stylesheet" type="text/css" href="test.css">

    使用 DOM 代码来创建这个元素:

    var link = document.createElement("link");
    link.rel = "stylesheet";
    link.type = "text/css";
    link.href = "test.css";
    var head = document.getElementByTagName("head")[0];
    head.appendChild(link);

    整个过程可以用一个函数进行封装:

    function loadStyle(url) {
    var link = document.createElement("link");
    link.rel = "stylesheet";
    link.type = "text/css";
    link.href = url;
    var head = document.getElementByTagName("head");
    head.appendChild(link);
    }
    // 调用函数加载外部样式
    loadStyle("test.css");

    加载外部样式文件的过程是异步的,也就是说加载样式与执行 JavaScript 代码的过程没有固定的次序。

  2. 通过 <style> 标签包含嵌入式样式
    我们以下面的这个 <style> 元素为例:

    <style rel="stylesheet" type="text/css">
    body {
    background-color: red;
    }
    </style>

    使用 DOM 代码来创建这个元素:

    var style = document.createElement("style");
    style.rel = "stylesheet";
    style.type = "text/css";
    style.appendChild(document.createTextNode("body {background-color: red;}"));
    var head = document.getElementByTagName("head");
    head.appendChild(style);

    <script> 类似,IE 将 <style> 视为一个特殊的元素节点,不允许 DOM 访问其子节点。解决 IE 中的这个问题,需要先访问元素的 styleSheet 属性,然后再访问 styleSheet 属性的 cssText 属性,这个属性可以接受 CSS 代码:

    function loadStyleString(css) {
    var style = document.createElement("style");
    style.rel = "stylesheet";
    style.type = "text/css";
    try {
    style.appendChild(document.createTextNode(css));
    } catch (error) {
    style.styleSheet.cssText = css;
    }
    var head = document.getElementByTagName("head")[0];
    head.appendChild(style);
    // 然后调用这个函数
    loadStyleString("body{background-color: red}");
    }

    这种方式会实时地向页面中添加样式,因此能够马上看到变化。

3. 使用 NodeList、HTMLCollection 和 NamedNodeMap

理解 NodeList、 HTMLCollection 和 NamedNodeMap 这三个集合,是从整体上理解 DOM 的关键。这三个集合都是动态的。每当文档结构发生变化时,它们都会得到更新。因此,它们始终都会保存最新,最准确的信息。从本质上说,执行流每经过一个集合,都会对 DOM 文档实行一次查询,从而获取最新的信息。例如,下面的代码会导致无限循环:

var divs = document.getElementByTagName("div"),
i,
div;
for (i=0; i < divs.length; i++) {
div = document.createElement("div");
document.body.appendChild(div);
}

for 循环每进行一次循环前都要对条件 i < divs.length 进行求值,意味着会对文档中所有的 div 元素进行一次查询,然后 i 跟这个新集合的 length 比较。所以虽然 i 在增加,可是 divs.length 同样在实时增加。这个循环会一直进行下去。

如果想要迭代一个这样的集合,最好先将集合的 length 属性初始化一个变量,这样这个变量就保存了一个这个 length 属性的副本。然后让迭代器与该变量进行比较,如下面的例子所示:

var divs = document.getElementByTagName("div"),
i,
len,
div;
for (i=0, len=divs.length; i < len; i++ ) {
div = document.createElement("div");
document.body.appendChild(div);
}

一般来说,应该尽量减少访问集合的次数。因为每次访问集合,都会执行一次基于文档的查询。所以,可以考虑将从集合中取得的值缓存起来。

本文参考:
JavaScript 高级程序设计(第3版)